package controller;

import static view.Constants.BUFFER_SIZE;
import static view.Constants.MAPPING;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Hashtable;

import core.CompressedChar;
import core.FileAnalysis;
import core.FileCompresser;
import core.FileDecompresser;
import core.Node;

import view.MainWindow;

/**
 * @author Mostafa Mahmod Mahmod Eweda
 * <pre>mostafa_eweda17@yahoo.com</pre>
 * @since JDK 1.6
 * @version 1.0
 * 
 * The controller that provides an intermediate layer between the UI and the core classes
 * and manages the easy manipulation from the UI side.
 * 
 * @see CompressedChar
 * @see FileCompresser
 * @see FileAnalysis
 * @see FileDecompresser
 */
public class Controller {

	private static Controller instance;

	private FileCompresser compresser;
	private MainWindow ui;

	/** 
	 * The size of the file in bits;
	 */
	private long opFileSize;

	/**
	 * The Table that has the cascaded chars to be written directly to the file
	 */
	private CompressedChar[] table;

	/**
	 * The Table that provides hashed access --> one instruction access to the
	 * chars by their ASCII code
	 */
	private CompressedChar[] chars;

	/**
	 * The root of the generated Huffman tree
	 */
	private Node root;

	/**
	 * Manages the data between the parts of the controller
	 */
	private Hashtable<String, Object> data;


	public static Controller getInstance()
	{
		return instance;
	}

	/**
	 * Creates a controller
	 */
	public Controller(MainWindow ui)
	{
		this.ui = ui;
		data = new Hashtable<String, Object>();
		instance = this;
	}

	/**
	 * Compresses the file given its path to the desired output path file
	 * @param source the source file path
	 * @param compressed the desired output path
	 */
	public void compress(String source, String compressed)
	{
		choooseBestCompression(source, compressed);
	}

	private void choooseBestCompression(String source, String compressed)
	{
		long required1 = 0; // the space required to save the file with the generated table
		long required2 = 0; // the space required to save the file with the default table
		CompressedChar[] defaultTable;
		try {
			required1 = computeRequiredSize(table, source);
		} catch (IOException e1) {
			e1.printStackTrace();
		}
		try {
			ObjectInputStream inTable;
			inTable = new ObjectInputStream(new FileInputStream("default table.3ew"));
			defaultTable = (CompressedChar[]) inTable.readObject();
			CompressedChar[] temp = new CompressedChar[256];
			for (int i = 0; i < 256; i++)
				temp[i] = chars[i];
			data.put("chars", temp);
			for (int i = 0; i < 256; i++)
				chars[defaultTable[i].ch + MAPPING] = null;
			for (int i = 0; i < defaultTable.length; i++)
				chars[defaultTable[i].ch + MAPPING] = defaultTable[i];
			inTable.close();
			required2 = computeRequiredSize(null, source);
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
//		required1 += 100000000;
		// Saving file with the table is less than the default table

		opFileSize = Math.min(required1, required2);

		if (required1 < required2)
		{
			chars = (CompressedChar[]) data.get("chars");
			compresser = new FileCompresser(source, compressed);
			compresser.writeTable();
			compresser.compress();
		}

		// Saving file with the default is better than Saving it the generated table
		else
		{
			compresser = new FileCompresser(source, compressed);
			compresser.noTable();
			compresser.compress();
		}
	}

	private long computeRequiredSize(CompressedChar[] table, String source) throws IOException
	{
		int numRead = 0;
		long required = 0;
		BufferedInputStream in = new BufferedInputStream(new FileInputStream(source));
		byte[] buffer = new byte[BUFFER_SIZE];
		if (table != null)
		{
			required += 2 * table.length;
		}
		while ((numRead = in.read(buffer)) != -1)
		{
			for (int i = 0; i < numRead; i++)
			{
				required += chars[buffer[i] + MAPPING].length;
			}
		}
		in.close();
		return required;
	}

	/**
	 * de-compresses the compressed file given its path tp the source main file back
	 * @param compressed the path of the compressed file
	 * @param decompressPath the trimmed path of the compressed file wanted to be created
	 */
	public void decompress(String compressed, String decompressPath)
	{
		FileDecompresser decompresser = new FileDecompresser(compressed, decompressPath);
		decompresser.registerTable();
		decompresser.decompress();
	}

	/**
	 * Registers analysis data for the given source file
	 */
	public void analyse(String source)
	{
		FileAnalysis analyse = new FileAnalysis(source);
		chars = analyse.getChars();
		analyse.collectStatistics();
		// System.out.println();
		Node[] nodes = analyse.constructStatistics();
		root = analyse.constructTree(nodes); // generate the root of the tree
		table = analyse.constructTable();
	}

	/**
	 * @return the root of the Huffman tree
	 */
	public Node getRoot()
	{
		return root;
	}

	/**
	 * @param root
	 * sets the root of the Huffman tree
	 */
	public void setRoot(Node root)
	{
		this.root = root;
	}

	/**
	 * @return chars
	 * @see #chars
	 */
	public CompressedChar[] getChars()
	{
		return chars;
	}

	/**
	 * @return table
	 * @see #table
	 */
	public CompressedChar[] getTable()
	{
		return table;
	}

	public void updateReached(long reached, long length)
	{
		ui.updateProgressBar(reached, length);
	}

	public double getCompressionRatio()
	{
		return (opFileSize / 8) / compresser.getSourceFileSize();
	}
}
